/*
* jDCBot.java
*
* Copyright (C) 2005 Kokanovic Branko
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.elite.jdcbot.framework;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.List;
import java.util.StringTokenizer;
import javax.imageio.IIOException;
import org.elite.jdcbot.shareframework.SearchResultSet;
import org.elite.jdcbot.shareframework.SearchSet;
import org.elite.jdcbot.shareframework.ShareManager;
import org.elite.jdcbot.util.GlobalFunctions;
import org.slf4j.Logger;
/**
* jDCBot is a Java framework for writing DC (direct connect) bots easily.
* <p>
* It provides an event-driven architecture to handle common DC events. For event-listener architecture see {@link EventjDCBot}.
* <p>
* Methods of the jDCBot class can be called to send events to the DC hub that it connects to. For example, calling the SendPublicMessage method will
* send a public message.
* <p>
* To perform an action when the jDCBot receives a normal message from the hub, you would override the onPubicMessage method defined in the jDCBot
* class. All on<i>XYZ</i> methods in the jDCBot class are automatically called when the event <i>XYZ</i> happens, so you would override these if
* you wish to do something when it does happen.
* <p>
* This class is thread safe.
*
* @author Kokanovic Branko
* @author AppleGrew
* @since 0.5
* @version 1.1.0
*/
public abstract class jDCBot extends InputThreadTarget implements UDPInputThreadTarget, BotInterface {
private static final Logger logger = GlobalObjects.getLogger(jDCBot.class);
protected int MAX_RESULTS_ACTIVE = 20;
protected int MAX_RESULTS_PASSIVE = 10;
/**
* Version of DC++ protocol being used.
*/
protected static final String _protoVersion = "1.0091";
protected InputThread _inputThread = null;
/**
* In multiHubsMode only the MultiHubsAdapter does the UDP listening.
*/
protected UDPInputThread _udp_inputThread = null;
private BotEventDispatchThread dispatchThread = null;
protected JobThread outThread;
private JobThread searchThread;
protected String miscDir;
protected String incompleteDir;
protected BufferedSocket socket = null;
protected BufferedServerSocket socketServer = null;
protected DatagramSocket udpSocket = null;
InputStream input;
OutputStream output;
protected UserManager um;
protected DownloadManager downloadManager;
protected UploadManager uploadManager;
protected boolean isTerminated = false;
/**
* In constructor a default ShareManager is
* instantiated. This can be set to a custom
* sub-class of ShareManager if needed after
* calling jDCBot's constructor by. You are
* advised to use the {@link #setShareManager(ShareManager)}
* method instead of directly assigning to it
* since the aforementioned method will also
* set <i>_sharesize</i> to proper value.
* <p>
* Note, that when MultiHubsAdapter is used then
* you will want to have one ShareManager for
* all jDCBot instances, hence use
* {@link MultiHubsAdapter#setShareManager(ShareManager)}
* instead.
*/
protected ShareManager shareManager;
private MultiHubsAdapter multiHubsAdapter = null;
/**
* Unlike for <i>shareManager</i>, DownloadCentral
* is not instantiated automatically. Classes extending
* jDCBot will have to instantiate it (if they need
* it).
*/
protected DownloadCentral downloadCentral = null;
protected String _botname, _password, _description, _conn_type, _email, _sharesize, _hubname;
protected boolean _passive;
protected InetAddress _ip;
/**
* This is set to hostname
* argument of {@link #connect(String, int)}.
* This is used to create hub signature.
*/
protected String _hubHostname;
protected int _port;
protected int _udp_port;
protected String _botIP;
protected int _listenPort;
protected int _maxUploadSlots;
protected int _maxDownloadSlots;
public static final String _hubproto_supports = "NoGetINFO UserIP2 MiniSlots TTH";
public static final String _clientproto_supports = "MiniSlots ADCGet XmlBZList TTHF ZLIG";
private String _hubSupports = "";
protected boolean _op = false;
//******Constructors******/
/**
* Constructs a jDCBot with your settings.
* <p>
* Most setting here depends on your hub. You might have to fake your share size and/or slots for hub to accept you... For details, look at <a
* href="http://www.teamfair.info/wiki/index.php?title=%24MyINFO">DC protocol wiki page of $MyINFO command</a>
*
*
* @param botname Name of the bot as it will appear in the list of users.
* @param botIP Your IP.
* @param listenPort The port on your computer where jdcbot should listen for incoming connections from clients.
* @param password Password if required, you could put anything if no password is needed.
* @param description Description of your bot as it will appear in the list of users. On your description is appended standard description.
* @param conn_type Your connection type, for details look <a href="http://www.teamfair.info/wiki/index.php?title=%24MyINFO">here</a>.
* <b>Note</b> that this setting is just a mere imitation. It will not actually limit upload speed.
* See {@link org.elite.jdcbot.shareframework.ShareManager#getUploadStreamManager() getUploadStreamManager()} for that.
* @param email Your e-mail address as it will appear in the list of users.
* @param sharesize Size of your share in bytes.
* @param uploadSlots Number of upload slots for other user to connect to you.
* @param downloadSlots Number of download slots. This has nothing to do with DC++ protocol. This has been given
* to put an upper cap on no. of simultaneous downloads.
* @param passive Set this to false if you are not behind a firewall.
* @throws IOException When error occurs trying to listen for port. The most probable reason for this would be that the port is not free.
* @throws BotException In case of invalid botname this is thrown.
*/
public jDCBot(String botname, String botIP, int listenPort, int UDP_listenPort, String password, String description, String conn_type,
String email, String sharesize, int uploadSlots, int downloadSlots, boolean passive) throws IOException, BotException {
init(botname, botIP, listenPort, UDP_listenPort, password, description, conn_type, email, sharesize, uploadSlots, downloadSlots,
passive, null, null);
}
/**
* Constructs a jDCBot with the default settings. Your own constructors in classes which extend the jDCBot abstract class should be
* responsible for changing the default settings if required.
* @throws IOException When error occurs trying to listen for port. The most probable reason for this would be that the port is not free.
*/
public jDCBot(String botIP) throws IOException {
init("jDCBot", botIP, 9000, 10000, "", "", "LAN(T1)" + User.NORMAL_FLAG, "", "0", 1, 3, false, null, null);
}
/**
* Creates a new jDCBot instance which can coexist with other jDCBot instances, all
* sharing the sharable resources like the server sockets, etc.
* @param multiHubsAdapter An instance of MultiHubsAdapter.
* @throws IOException When error occurs trying to listen for port. The most probable reason for this would be that the port is not free.
*/
public jDCBot(MultiHubsAdapter multiHubsAdapter) throws IOException {
if(multiHubsAdapter == null) {
throw new NullPointerException("Supplied MultiHubsAdapter is null.");
}
this.multiHubsAdapter = multiHubsAdapter;
this.multiHubsAdapter.addBot(this);
init(multiHubsAdapter.botname(this), multiHubsAdapter._botIP, multiHubsAdapter._listenPort, multiHubsAdapter._udp_port,
multiHubsAdapter.getPassword(this), multiHubsAdapter.getDescription(this), multiHubsAdapter.getConnType(this),
multiHubsAdapter.getEmail(this), multiHubsAdapter._sharesize, multiHubsAdapter._maxUploadSlots,
multiHubsAdapter._maxDownloadSlots, multiHubsAdapter.isPassive(this), multiHubsAdapter.shareManager,
multiHubsAdapter.downloadCentral);
}
/**
* Constructs a jDCBot.
* @param config
* @throws IOException When error occurs trying to listen for port. The most probable reason for this would be that the port is not free.
* @throws BotException In case of invalid botname this is thrown.
*/
public jDCBot(BotConfig config) throws IOException, BotException {
init(
config.getBotname(),
config.getBotIP(),
config.getListenPort(),
config.getUDP_listenPort(),
config.getPassword(),
config.getDescription(),
config.getConn_type(),
config.getEmail(),
config.getSharesize(),
config.getUploadSlots(),
config.getDownloadSlots(),
config.isPassive(),
null, null);
}
private void init(String botname, String botIP, int listenPort, int UDP_listenPort, String password, String description,
String conn_type, String email, String sharesize, int uploadSlots, int downloadSlots, boolean passive,
ShareManager share_manager, DownloadCentral dc) throws IOException, BotException {
if(!GlobalFunctions.isUserNameValid(botname)) {
throw new BotException(BotException.Error.INVALID_USERNAME);
}
if (isInMultiHubsMode()) {
miscDir = multiHubsAdapter.miscDir;
incompleteDir = multiHubsAdapter.incompleteDir;
}
_botname = botname;
_password = password;
_conn_type = conn_type;
_email = email;
_sharesize = sharesize;
_maxUploadSlots = uploadSlots;
_maxDownloadSlots = downloadSlots;
_botIP = botIP;
_listenPort = listenPort;
_passive = passive;
_udp_port = UDP_listenPort;
um = new UserManager(this);
downloadManager = new DownloadManager(this);
uploadManager = new UploadManager(this);
dispatchThread = new BotEventDispatchThread(this);
setShareManager(share_manager, _sharesize);
setDownloadCentral(dc);
if (_sharesize == null || _sharesize.isEmpty())
_sharesize = "0";
setDescription(description);
//Creating Listen port for clients to contact this.
if (isInMultiHubsMode())
socketServer = multiHubsAdapter.socketServer;
if (socketServer == null || socketServer.isClosed()) {
socketServer = new BufferedServerSocket(_listenPort);
socketServer.setSoTimeout(60000); // Wait for 60s before timing out.
}
initiateUDPListening();
outThread = new JobThread();
outThread.setDaemon(true);
outThread.start();
searchThread = new JobThread();
searchThread.setDaemon(true);
searchThread.start();
}
final public void setBotName(String botname) throws BotException {
if(!GlobalFunctions.isUserNameValid(botname)) {
throw new BotException(BotException.Error.INVALID_USERNAME);
}
_botname = botname;
}
final public void setPassword(String pass) {
_password = pass;
}
final public void setEmail(String email) {
_email = email;
}
final public String getBotName() {
return _botname;
}
final public String getPassword() {
return _password;
}
final public String getDescription() {
return _description;
}
/**
*
* @param description
* @param totalHubsConnectedTo
*/
final public void setDescription(String description, int totalHubsConnectedToNormalUsers,
int totalHubsConnectedToAsOps, int totalHubsConnectedToAsRegistered) {
this._description = new StringBuffer(description)
.append("<++ V:").append(GlobalObjects.VERSION).append(",M:")
.append((isPassive() ? 'P' : 'A')).append(",H:").append(totalHubsConnectedToNormalUsers)
.append('/').append(totalHubsConnectedToAsRegistered).append('/').append(totalHubsConnectedToAsOps)
.append(",S:").append(getMaxUploadSlots()).append(">").toString();
}
/**
* Sets description with appropriate tags.
* @param description
*/
public void setDescription(String description) {
int opsCount = 0, regCount = 0, normalCount = 0;
if(multiHubsAdapter != null) {
opsCount = multiHubsAdapter.getTotalHubsConnectedToAsOps();
regCount = multiHubsAdapter.getTotalHubsConnectedToAsRegistered();
normalCount = multiHubsAdapter.getTotalHubsConnectedToCount() - opsCount - regCount;
if(normalCount < 0) {
normalCount = multiHubsAdapter.getTotalHubsConnectedToCount();
}
} else {
if(isOp()) {
opsCount = 1;
} else if(isRegistered()) {
regCount = 1;
} else {
normalCount = 1;
}
}
setDescription(description, normalCount, opsCount, regCount);
}
final public String getConnType() {
return _conn_type;
}
final public void setConnType(String conn_type) {
this._conn_type = conn_type;
}
final public String getEmail() {
return _email;
}
final public String getBotIP() {
return _botIP;
}
final public void setBotIP(String botIP) {
this._botIP = botIP;
}
/**
* <b>Note:</b> The given directories' must exist and should be empty, as
* already existing files in them will overwritten without warning.
* @param path2DirForMiscData In this directory own file list, hash data, etc. will be kept.
* @param path2IncompleteDir Where incomplete downloads will be kept.
* @throws FileNotFoundException If the directory paths are not found or 'fileListHash'
* doesn't exist.
* @throws IIOException If the given path are not directories.
* @throws InstantiationException The read object from 'fileListHash' is not instance of FLDir.
* @throws ClassNotFoundException Class of FLDir serialized object cannot be found.
* @throws IOException Error occurred while reading from 'fileListHash'.
*/
final public void setDirs(String path2DirForMiscData, String path2IncompleteDir) throws IIOException, FileNotFoundException,
IOException {
File miscDir = new File(path2DirForMiscData);
File incompleteDir = new File(path2IncompleteDir);
if (!miscDir.exists() || !incompleteDir.exists())
throw new FileNotFoundException();
if (!miscDir.isDirectory())
throw new IIOException("Given path '" + path2DirForMiscData + "' is not a directory.");
if (!incompleteDir.isDirectory())
throw new IIOException("Given path '" + path2IncompleteDir + "' is not a directory.");
this.miscDir = miscDir.getCanonicalPath();
this.incompleteDir = incompleteDir.getCanonicalPath();
}
//******Methods to get informations or other misc getters******/
/**
* @return Name of the bot
*/
final public String botname() {
return _botname;
}
/**
* @return <b>true</b> if the bot/client is in Passive mode. <b>false</b> if it is in Active mode.
*/
final public boolean isPassive() {
return _passive;
}
/**
* @return Returns if the bot is Operator or not.
*/
final public boolean isOp() {
return _op;
}
/**
* @return true if password is set. Logic is that
* the user would never supply a password if he was
* not registered at hub in the first place.
*/
final public boolean isRegistered() {
return _password != null;
}
/**
* @return Name of the hub we're connected on
*/
public final String hubname() {
return _hubname;
}
final public String getBotHubProtoSupports() {
return _hubproto_supports;
}
final public String getBotClientProtoSupports() {
return _clientproto_supports;
}
final public String getHubSupports() {
return _hubSupports;
}
public ShareManager getShareManager() {
return shareManager;
}
public DownloadCentral getDownloadCentral() {
return downloadCentral;
}
final BotEventDispatchThread getDispatchThread() {
return dispatchThread;
}
public String getMiscDir() {
return miscDir;
}
public String getIncompleteDir() {
return incompleteDir;
}
/**
* Returns whether or not the jDCBot is currently connected to a hub. The result of this method should only act as a rough guide, as the
* result may not be valid by the time you act upon it.
*
* @return true if and only if the jDCBot is currently connected to a hub.
*/
final public boolean isConnected() {
return _inputThread != null;
}
/**
* Checks if the hub supports that protocol feature.
* @param feature
* @return
*/
final public boolean isHubSupports(String feature) {
return _hubSupports.toLowerCase().indexOf(feature.toLowerCase()) != -1;
}
/**
* Checks if the bot's client-hub protocol implementation supports that protocol feature.
* @param feature
* @return
*/
final public boolean isBotHubProtoSupports(String feature) {
return _hubproto_supports.toLowerCase().indexOf(feature.toLowerCase()) != -1;
}
/**
* Checks if the bot's client-client protocol implementation supports that protocol feature.
* @param feature
* @return
*/
final public boolean isBotClientProtoSupports(String feature) {
return _clientproto_supports.toLowerCase().indexOf(feature.toLowerCase()) != -1;
}
public int getMaxUploadSlots() {
return _maxUploadSlots;
}
public int getMaxDownloadSlots() {
return _maxDownloadSlots;
}
public int getFreeUploadSlots() {
if (isInMultiHubsMode())
return multiHubsAdapter.getFreeUploadSlots();
else {
int free = _maxUploadSlots - uploadManager.getAllUHCount();
return free >= 0 ? free : 0;
}
}
public int getFreeDownloadSlots() {
if (isInMultiHubsMode())
return multiHubsAdapter.getFreeDownloadSlots();
else {
int free = _maxDownloadSlots - downloadManager.getAllDHCount();
return free >= 0 ? free : 0;
}
}
final public boolean isInMultiHubsMode() {
return multiHubsAdapter != null;
}
/**
* Checks if user is present on hub
*
* @param user
* Nick of a user
* @return true if user exist on this hub, false otherwise
*/
synchronized final public boolean UserExist(String user) {
if (!isConnected())
return false;
return um.userExist(user);
}
/**
* Gets all of user info
*
* @param user Nick of the user
* @return User class that holds everything about specified user if he exists, null otherwise
*/
synchronized final public User getUser(String user) {
if (!isConnected() || um.userExist(user) == false)
return null;
else
return um.getUser(user);
}
/**
* @return User with the matching client ID. If none found then it is null.
*/
synchronized final public User getUserByCID(String CID) {
if (!isConnected())
return null;
return um.getUserByCID(CID);
}
/**
* This uniquely identifies a hub. This
* is nothing more than hub's ip
* concatenated with its port.<br>
* If hub ip is (say) 127.0.0.1 and
* port is 411 then output is<br>
* <code>127.0.0.1:411</code>
* @return The unique signature of the hub.
*/
synchronized final public String getHubSignature() {
return Hub.prepareHubSignature(_hubHostname, _port);
}
/**
*
* @return Random user from the hub
*/
final public User GetRandomUser() {
return um.getRandomUser();
}
public final User[] GetAllUsers() {
return um.getAllUsers();
}
//******Methods that perform some tasks******/
public final void connect(InetAddress hubHost, int port) throws IOException, BotException {
connect(hubHost.getHostAddress(), port);
}
/**
* Attempt to connect to the specified DC hub. The OnConnect method is called upon success.
*
* @param hostname The hostname of the server to connect to.
*
* @throws IOException
* if it was not possible to connect to the server.
* @throws BotException
* if the server would not let us join it because of bad password or if there exist user with the same name.
*/
synchronized public final void connect(String hostname, int port) throws IOException, BotException {
if (isTerminated) {
throw new IOException("Bot terminated. You can no longer call connect(). Create a new instance.");
}
String buffer;
_port = port;
if (this.isConnected()) {
throw new IOException("Already connected");
}
_hubHostname = hostname;
// connect to server
socket = new BufferedSocket(hostname, port);
input = socket.getInputStream();
output = socket.getOutputStream();
//breader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
_ip = socket.getInetAddress();
buffer = ReadCommand();
String lock = parseRawCmd(buffer)[1];
if (lock.startsWith("EXTENDEDPROTOCOL")) {
buffer = "$Supports " + _hubproto_supports + "|";
SendCommand(buffer);
}
String key = lock2key(lock);
buffer = "$Key " + key + "|";
SendCommand(buffer);
buffer = "$ValidateNick " + _botname + "|";
SendCommand(buffer);
buffer = ReadCommand();
int maxIters = 30;
while (buffer.startsWith("$Hello") != true) {
if (buffer.startsWith("$ValidateDenide"))
throw new BotException(BotException.Error.VALIDATE_DENIED);
if (buffer.startsWith("$BadPass"))
throw new BotException(BotException.Error.BAD_PASSWORD);
if (buffer.startsWith("$GetPass")) {
buffer = "$MyPass " + _password + "|";
SendCommand(buffer);
} else
if (buffer.startsWith("$HubName ")) {
_hubname = unescapeSpecial(buffer.substring(9, buffer.length() - 1));
dispatchThread.callOnHubName(_hubname);
} else
if (buffer.startsWith("$Supports ")) {
_hubSupports = parseCmdArgs(buffer);
} else
if (buffer.startsWith("<")) {
processPublicMsg(buffer);
}
maxIters--;
if(maxIters < 0) {
throw new BotException("Hub taking too long to handshake. Aborting!",
BotException.Error.TIMEOUT);
}
try {
buffer = ReadCommand();
} catch (IOException e) {
//Sends the last read command (this will usually be a message from the hub).
throw new BotException(e.getMessage() + ": " + buffer, BotException.Error.IO_ERROR);
}
}
buffer = "$Version " + _protoVersion + "|";
SendCommand(buffer);
buffer = "$GetNickList|";
SendCommand(buffer);
sendMyINFO();
_inputThread = new InputThread(this, input, "Hub InputThread");
_inputThread.start();
dispatchThread.callOnConnect();
}
synchronized protected void sendMyINFO() throws IOException {
String buffer = "$MyINFO $ALL " + _botname + " " + _description + "$ $" + _conn_type + "$" + _email + "$" + _sharesize + "$|";
SendCommand(buffer);
}
synchronized final Socket initConnectToMe(String user, String direction) throws BotException, IOException {
if (!isConnected()) {
throw new BotException(BotException.Error.NOT_CONNECTED_TO_HUB);
}
if (!UserExist(user)) {
throw new BotException(BotException.Error.USERNAME_NOT_FOUND);
}
direction = direction.toLowerCase();
direction = direction.substring(0, 1).toUpperCase() + direction.substring(1);
Socket newsocket = null;
String buffer = null;
User u = um.getUser(user);
//connect to client
try {
if (isInMultiHubsMode()){
/*
* The statement below is needed, so that multiple bots'
* simultaneous request for connection can be synchronized
* because ServerSocket though allows multiple threads to
* simultaneously listen but the packet received will be sent
* to any one of the threads, maybe the one that requested to
* listen first gets the first packet, but suppose botA sends
* issues CTM to client A and now it starts listening for the
* connection, and at the same time botB also issues CTM to
* client B and it too waits for incoming. If due to heavy
* network traffic on client A's route its packet is delayed and
* hence client B's response is received earlier then botB will
* end up with connection to client A and botA with client B.
* While verifying remote nicks they both find it wrong and
* hence both the connections will be dropped.
*
* To fix that we synchronized the threads using locks.
*/
multiHubsAdapter.lock.lock();
}
try {
buffer = "$ConnectToMe " + user + " " + _botIP + ":" + _listenPort + "|";
SendCommand(buffer);
newsocket = socketServer.accept();
} catch (SocketTimeoutException soce) {
logger.error("Connection to client " + user + " timed out.", soce);
throw soce;
}
} finally {
if (isInMultiHubsMode())
multiHubsAdapter.lock.unlock(); //We can now safely unlock as connection has been made.
}
String remoteClientIP = newsocket.getInetAddress().getHostAddress();
logger.info("00>>Connected to remote Client:: " + remoteClientIP);
u.setUserIP(remoteClientIP);
final InputStream clientInput = newsocket.getInputStream();
buffer = ReadCommand(clientInput); //Reading $MyNick remote_nick| OR
//$MaxedOut //remote_user == user
if (buffer.equals("$MaxedOut|")) {
throw new BotException(BotException.Error.NO_FREE_SLOTS);
}
String remote_nick = parseRawCmd(buffer)[1];
if (!remote_nick.equalsIgnoreCase(user)) {
logger.error("Remote client wrong username. Expected: " + user + ", but got: " + remote_nick);
throw new BotException(BotException.Error.REMOTE_CLIENT_SENT_WRONG_USERNAME);
}
buffer = ReadCommand(clientInput); //Reading $Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.698ABCABC|
String lock = parseRawCmd(buffer)[1];
String key = lock2key(lock);
if (!lock.startsWith("EXTENDEDPROTOCOL")) {
logger.warn("Using non-Extended protocol. What kind of old client is this! You can expect errors now.");
buffer = "$Key " + key + "|";
SendCommand(buffer, newsocket);
// We are now required to send a lock to the remote client. I am
// simply resending the same lock it returned, back to it.
buffer = lock;
SendCommand(buffer, newsocket);
buffer = ReadCommand(clientInput); // Read the key sent by the
// remote client. I am not
// verifying the key,
// hence I am now simply moving on to the next step without
// parsing
// buffer.
buffer = "$MyNick " + _botname + "|";
SendCommand(buffer, newsocket);
buffer = ReadCommand(clientInput); // Reading $Direction.
String read_direction = buffer.substring(buffer.indexOf(' ') + 1, buffer.indexOf(' ', buffer.indexOf(' ') + 1));
if (read_direction.equalsIgnoreCase(direction)) {
// In this case the remote client to wants to do the same thing as this client, i.e.
// both wants to download or upload.
logger.warn("WARNING! Remote client for " + user + " too wants to " + direction
+ " from me. This situation is not handled.");
}
} else {
logger.info("Using Extended protocol.");
int N1 = 0x7FFF - 2; //Cheating! Cheating! This value should be randomly generated, but here I am setting this to max possible value - 2.
buffer = new StringBuffer
("$MyNick ").append(_botname).append("|$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.698ABCABC|$Supports ")
.append(_clientproto_supports).append("|$Direction ").append(direction).append(" ").append(N1).append("|$Key ")
.append(key).append("|").toString();
SendCommand(buffer, newsocket);
buffer = ReadCommand(clientInput);// Reading $Supports S|
String remote_supports = parseCmdArgs(buffer);
u.setSupports(remote_supports);
buffer = ReadCommand(clientInput);// Reading $Direction Upload N2|
String params[] = parseRawCmd(buffer);
int N2 = Integer.parseInt(params[2]);
if (params[1].equalsIgnoreCase(direction)) {
logger.warn("Huh! Remote client for " + user + " too wants to " + direction + " from me. This situation is not handled.");
}
buffer = ReadCommand(clientInput);// Reading $Key ........A .....0.0. 0. 0. 0. 0. 0.|
if (N1 < N2)
logger.warn("N1 is < N2 dunno what to do now. Anyway continuing as if it never happened.");
}
dispatchThread.callOnConnect2Client();
return newsocket;
}
synchronized private void replyConnectToMe(String user, String ip, int port) throws BotException, IOException { //Called in response to $ConnectToMe command from hub.
if (!isConnected()) {
throw new BotException(BotException.Error.NOT_CONNECTED_TO_HUB);
}
if (!user.equalsIgnoreCase(_botname)) {
throw new BotException(BotException.Error.UNEXPECTED_RESPONSE);
}
Socket newsocket = new Socket();
newsocket.connect(new InetSocketAddress(ip, port), 60000);
logger.info("00>>Connected to remote Client:: " + ip + ":" + port);
String buffer = "$MyNick " + _botname + "|$Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.698ABCABC|"; //user == remote_nick
SendCommand(buffer, newsocket);
final InputStream clientInput = newsocket.getInputStream();
buffer = ReadCommand(clientInput); //Reading $MyNick remote_nick| OR $MaxedOut
if (buffer.equals("$MaxedOut|")) {
throw new BotException(BotException.Error.NO_FREE_SLOTS);
}
String remote_nick = parseRawCmd(buffer)[1];
if (!um.userExist(remote_nick)) {
throw new BotException(BotException.Error.USERNAME_NOT_FOUND);
}
User u = um.getUser(remote_nick);
u.setUserIP(ip);
buffer = ReadCommand(clientInput); //Reading $Lock EXTENDEDPROTOCOLABCABCABCABCABCABC Pk=DCPLUSPLUS0.698ABCABC|
String lock = parseRawCmd(buffer)[1];
String key = lock2key(lock);
if (!lock.startsWith("EXTENDEDPROTOCOL")) {
logger.error("Remote client doesn't support Exdtended protocol. Don't know how to continue now. Baling out.");
throw new BotException(BotException.Error.PROTOCOL_UNSUPPORTED);
}
buffer = ReadCommand(clientInput); //Reading $Supports S|
String remote_supports = parseCmdArgs(buffer);
u.setSupports(remote_supports);
buffer = ReadCommand(clientInput); //Reading $Direction D N|
String params[] = parseRawCmd(buffer);
String direction = params[1];
int N = Integer.parseInt(params[2]);
buffer = ReadCommand(clientInput); //Reading $Key ........A .....0.0. 0. 0. 0. 0. 0.|
if (direction.equalsIgnoreCase("Upload")) {
try {
downloadManager.download(remote_nick, newsocket, N, key);
} catch (BotException be) {
socket.close();
}
} else
uploadManager.upload(remote_nick, newsocket, N, key);
}
/**
* Attempts to nicely close connection with the hub. You
* can call {@link #connect(String, int) connect} again to connect to the hub.
*/
final public void quit() {
outThread.invokeLater(new Runnable() {
@Override
public void run() {
try {
SendCommand("$Quit " + _botname + "|");
} catch (IOException e) {}
finally {
try {
if (_inputThread != null)
_inputThread.stop();
if (socket != null)
socket.close();
socket = null;
_inputThread = null;
} catch (IOException e) {}
dispatchThread.callOnBotQuit();
}
}
});
}
/**
* Call this when you want to shut down framework completely. Unlike {@link #quit() quit},
* {@link #connect(String, int) connect} is not supposed to be called after calling this method.
*/
public void terminate() {
if (isTerminated) {
return;
}
quit();
uploadManager.close();
downloadManager.close();
if (_inputThread != null)
_inputThread.stop();
dispatchThread.stopIt();
if (!isInMultiHubsMode() && _udp_inputThread != null)
_udp_inputThread.stop();
if (shareManager != null && !isInMultiHubsMode())
shareManager.close();
if (downloadCentral != null && !isInMultiHubsMode())
downloadCentral.close();
outThread.terminate();
if(multiHubsAdapter != null) {
multiHubsAdapter.removeBot(this);
}
if (!isInMultiHubsMode() && socketServer != null && !socketServer.isClosed()) {
try {
socketServer.close();
} catch (IOException e) {
logger.warn("Exception in terminate.", e);
}
}
isTerminated = true;
}
@Override
final void disconnected() {
_inputThread = null;
dispatchThread.callOnDisconnect();
}
/**
* Converts special characters like
* |, $, etc. to a form acceptable by
* DC protocol.
* <p>
* This method is reentrant.
* @param msg
* @param whitespace If true then replaces white spaces by +.
* @return
*/
final public static String escapeSpecial(String msg, boolean whitespace) {
msg = msg.replace("&", "&").replace("$", "$").replace("|", "|");
if (whitespace)
msg = msg.replace(' ', '$');
return msg;
}
/**
* Converts escaped special characters back to original.
* <p>
* This method is reentrant.
* @param msg
* @return
*/
final public static String unescapeSpecial(String msg) {
return msg.replace('$', ' ').replace("$", "$").replace("|", "|").replace("&", "&");
}
/**
* This will convert forward slashes to backward slashes (as required by
* DC protocol). <b>Note:</b> It <u>won't</u> escape special characters
* automatically. You will need to manually call {@link #escapeSpecial(String, boolean)}.
* <p>
* This method is reentrant.
* @param path
* @return
*/
final public static String sanitizePath(String path) {
path = path.trim().replace('/', '\\');
if (path.startsWith("\\") && path.length() > 1)
path = path.substring(1);
return path;
}
/**
* Sets if the bot is operator or not.
* @param flag
*/
final void setOp(boolean flag) {
_op = flag;
}
/**
* Handled internally. <b>You DO NOT NEED TO CALL.</b>
* Although calling it won't hurt.
*/
public final void updateShareSize() {
String sharesize = String.valueOf(shareManager.getOwnShareSize(false));
if (sharesize.equals(_sharesize))
return;
_sharesize = sharesize;
try {
sendMyINFO();
} catch (IOException e) {
logger.error("Exception in updateShareSize()", e);
}
}
final void setPassive(boolean flag) {
if (_passive == flag)
return;
_passive = flag;
try {
sendMyINFO();
} catch (IOException e) {
logger.error("Exception in setPassive()", e);
}
}
final public void setMaxUploadSlots(int slots) {
_maxUploadSlots = slots;
}
final public void setMaxDownloadSlots(int slots) {
_maxDownloadSlots = slots;
}
/**
* <b>Note:</b> <u>Always</u> call {@link #setDirs(String, String)}
* before calling this method else you will get all sorts of nasty
* exceptions like NullPointerException, etc. and ShareManager
* will seem to be not working at all.
* <p>
* Same as {@link #setShareManager(ShareManager, String)}
* but this will also calculate the share size and
* set bot's share size to that value.
* @param sm
*/
final public void setShareManager(ShareManager sm) {
if (sm != null) {
setShareManager(sm, String.valueOf(sm.getOwnShareSize(false)));
}
}
/**
* <b>Note:</b> <u>Always</u> call {@link #setDirs(String, String)}
* before calling this method else you will get all sorts of nasty
* exceptions like NullPointerException, etc. and ShareManager
* will seem to be not working at all.
* @param sm
* @param sharesize
*/
final public void setShareManager(ShareManager sm, String sharesize) {
if (sm != null) {
shareManager = sm;
if (!isInMultiHubsMode()) {
shareManager.setDirs(miscDir);
shareManager.init();
}
_sharesize = sharesize;
}
}
/**
* <b>Note:</b> <u>Always</u> call {@link #setDirs(String, String)}
* before calling this method else you will get all sorts of nasty
* exceptions like NullPointerException, etc. and DownloadCentral
* will seem to be not working at all.
* @param dc
*/
final public void setDownloadCentral(DownloadCentral dc) {
if (dc != null) {
downloadCentral = dc;
if (!isInMultiHubsMode()) {
downloadCentral.setDirs(incompleteDir);
downloadCentral.init();
downloadCentral.startNewQueueProcessThread();
}
}
}
/**
* Handles all commands from InputThread all passes it to different methods.
*
* @param rawCommand Raw command sent from hub
*/
final void handleCommand(String rawCommand) {
if (rawCommand.startsWith("<")) {
processPublicMsg(rawCommand);
} else if (rawCommand.startsWith("$Quit")) {
String user = rawCommand.substring(6);
user = user.substring(0, user.length() - 1);
um.userQuit(user);
dispatchThread.callOnQuit(user);
} else if (rawCommand.startsWith("$Hello") && (rawCommand != "$Hello " + _botname + "|")) {
String user = rawCommand.substring(7);
user = user.substring(0, user.length() - 1);
um.userJoin(user);
dispatchThread.callOnJoin(user);
} else if (rawCommand.startsWith("$To:")) {
String user, from, message;
int index1 = rawCommand.indexOf('$', 2);
int index2 = rawCommand.indexOf('>', index1);
from = rawCommand.substring(rawCommand.indexOf(':', 4) + 2, rawCommand.indexOf('$', 2) - 1);
user = rawCommand.substring(index1 + 2, index2);
message = unescapeSpecial(rawCommand.substring(index2 + 2, rawCommand.length() - 1));
if (user.equals(from))
dispatchThread.callOnPrivateMessage(user, message);
else
dispatchThread.callOnChannelMessage(user, from, message);
} else if (rawCommand.startsWith("$Search ")) {
int space = rawCommand.indexOf(' ', 9);
String firstPart = rawCommand.substring(8, space).trim();
String secondPart = rawCommand.substring(space + 1, rawCommand.length() - 1);
StringTokenizer st = new StringTokenizer(secondPart, "?");
if (st.countTokens() != 5)
return;
boolean isSizeRestricted, isMinimumSize;
long size;
SearchSet.DataType dataType;
String searchPattern;
isSizeRestricted = (st.nextToken() == "T");
isMinimumSize = (st.nextToken() == "T");
size = Long.parseLong(st.nextToken());
dataType = SearchSet.DataType.getEnumForValue(Integer.parseInt(st.nextToken()));
searchPattern = st.nextToken();
SearchSet search = new SearchSet();
search.string = dataType == SearchSet.DataType.TTH ? searchPattern : unescapeSpecial(searchPattern);
search.size = size;
search.size_unit = SearchSet.SizeUnit.BYTE;
search.size_criteria =
isSizeRestricted ? (isMinimumSize ? SearchSet.SizeCriteria.ATMOST : SearchSet.SizeCriteria.ATLEAST)
: SearchSet.SizeCriteria.NONE;
search.data_type = dataType;
// send trigger to passive/active search
if (firstPart.toLowerCase().startsWith("hub:")) {
String user = firstPart.substring(4);
onSearch(user, search);
dispatchThread.callOnPassiveSearch(user, search);
onPassiveSearch(user, isSizeRestricted, isMinimumSize, size, dataType.getValue(), searchPattern);
} else {
int dotdot = firstPart.indexOf(':');
String ip = firstPart.substring(0, dotdot);
int port = Integer.parseInt(firstPart.substring(dotdot + 1));
onSearch(ip, port, search);
dispatchThread.callOnActiveSearch(ip, port, search);
onActiveSearch(ip, port, isSizeRestricted, isMinimumSize, size, dataType.getValue(), searchPattern);
}
} else if (rawCommand.startsWith("$NickList")) {
um.addUsers(rawCommand.substring(10, rawCommand.length() - 1));
} else if (rawCommand.startsWith("$OpList")) {
um.addOps(rawCommand.substring(8, rawCommand.length() - 1));
} else if (rawCommand.startsWith("$MyINFO $ALL")) {
String usr = rawCommand.substring(13, rawCommand.indexOf(' ', 13));
if (usr.equalsIgnoreCase(_botname))
return;
um.SetInfo(rawCommand.substring(13, rawCommand.length() - 1));
if (downloadCentral != null)
downloadCentral.triggerProcessQ(false);
} else if (rawCommand.startsWith("$UserIP")) {
um.updateUserIPs(rawCommand.substring(8, rawCommand.length() - 1));
} else if (rawCommand.startsWith("$RevConnectToMe")) {
String params[] = parseRawCmd(rawCommand);
String me = params[2];
String remote_user = params[1];
if (me.equalsIgnoreCase(_botname)) {
try {
uploadManager.uploadPassive(remote_user);
} catch (BotException e) {
logger.error("BotException from uploadManager.uploadPassive(): " + e.getMessage(), e);
}
}
} else if (rawCommand.startsWith("$ConnectToMe")) {
String params[] = parseRawCmd(rawCommand);
String user = params[1];
params = params[2].split(":");
String ip = params[0];
int port = Integer.parseInt(params[1]);
try {
replyConnectToMe(user, ip, port);
} catch (Exception e) {
logger.error("Exception by replyConnectToMe in handleCommand: " + e.getMessage(), e);
}
} else if (rawCommand.startsWith("$SR ")) {
processSRcommand(rawCommand, null, 0);
} else if (rawCommand.startsWith("$HubName ")) {
_hubname = unescapeSpecial(rawCommand.substring(9, rawCommand.length() - 1));
dispatchThread.callOnHubName(_hubname);
} else
logger.debug("The command above is not handled.");
}
private void processPublicMsg(String rawCommand) {
String user, message;
user = rawCommand.substring(1, rawCommand.indexOf('>'));
message = rawCommand.substring(rawCommand.indexOf('>'));
message = message.substring(2, message.length() - 1);
dispatchThread.callOnPublicMessage(user, unescapeSpecial(message));
}
/**
* Handled internally. <b>DO NOT CALL.</b>
*/
public final void handleUDPCommand(String rawCommand, String ip, int port) {
logger.debug("From user(" + ip + ":" + port + "): " + rawCommand);
if (rawCommand.startsWith("$SR ")) {
processSRcommand(rawCommand, ip, port);
} else
logger.debug("The command above is not handled.");
}
private void processSRcommand(String rawCommand, String ip, int port) {
int delil = rawCommand.indexOf(' ');
int delir = rawCommand.indexOf(' ', delil + 1);
String senderNick = rawCommand.substring(delil + 1, delir);
delil = delir;
delir = rawCommand.lastIndexOf(' ', rawCommand.lastIndexOf(5));
String result = rawCommand.substring(delil + 1, delir);
delil = delir;
delir = rawCommand.indexOf('/', delil + 1);
int free_slots = Integer.parseInt(rawCommand.substring(delil + 1, delir));
delil = delir;
delir = rawCommand.indexOf(5, delil + 1);
int total_slots = Integer.parseInt(rawCommand.substring(delil + 1, delir));
delil = delir;
delir = rawCommand.indexOf(' ', delil + 1);
String hubORTTH = rawCommand.substring(delil + 1, delir);
boolean isTTH = hubORTTH.startsWith("TTH:");
delil = rawCommand.indexOf('(', delir + 1);
delir = rawCommand.indexOf(')', delil + 1);
String hubIPANDPort = rawCommand.substring(delil + 1, delir);
String hubIP;
int hubPort = 411;
int colonPos = hubIPANDPort.indexOf(':');
if (colonPos != -1) {
hubIP = hubIPANDPort.substring(0, colonPos);
hubPort = Integer.parseInt(hubIPANDPort.substring(colonPos + 1));
} else
hubIP = hubIPANDPort;
if (!hubIP.equals(_ip.getHostAddress()) || hubPort != _port) {
logger.info("Search result meant for other hub has been ignored.");
return;
}
int res5Pos = -1;
boolean isDir = (res5Pos = result.indexOf(5)) == -1;
String resName = result;
long resSize = 0;
if (!isDir) {
resName = result.substring(0, res5Pos);
resSize = Long.parseLong(result.substring(res5Pos + 1));
}
SearchResultSet res = new SearchResultSet();
res.name = unescapeSpecial(resName);
res.size = resSize;
res.isDir = isDir;
res.TTH = isTTH ? hubORTTH.substring(4) : "";
if (ip != null) {
User u = getUser(senderNick);
if (u != null)
u.setUserIP(ip);
}
if (downloadCentral != null && isTTH)
downloadCentral.searchResult(res.TTH, getUser(senderNick));
dispatchThread.callOnSearchResult(senderNick, ip, port, res, free_slots, total_slots, isTTH ? "" : hubORTTH);
}
synchronized private void initiateUDPListening() throws SocketException {
if (isInMultiHubsMode())
return;
if (_udp_inputThread != null && !_udp_inputThread.isClosed())
return;
udpSocket = new DatagramSocket(_udp_port);
_udp_inputThread = new UDPInputThread(this, udpSocket);
_udp_inputThread.start();
}
/**
* Generates key from lock needed to connect to hub.
* <p>
* This method is re-entrant.
* @param lock Lock sent from hub
* @return Key which is sent back to hub to validate we know algorithm:-/
*/
private final String lock2key(String lock) {
String key_return;
int len = lock.length();
char[] key = new char[len];
for (int i = 1; i < len; i++)
key[i] = (char) (lock.charAt(i) ^ lock.charAt(i - 1));
key[0] = (char) (lock.charAt(0) ^ lock.charAt(len - 1) ^ lock.charAt(len - 2) ^ 5);
for (int i = 0; i < len; i++)
key[i] = (char) (((key[i] << 4) & 240) | ((key[i] >> 4) & 15));
key_return = new String();
for (int i = 0; i < len; i++) {
if (key[i] == 0) {
key_return += "/%DCN000%/";
} else if (key[i] == 5) {
key_return += "/%DCN005%/";
} else if (key[i] == 36) {
key_return += "/%DCN036%/";
} else if (key[i] == 96) {
key_return += "/%DCN096%/";
} else if (key[i] == 124) {
key_return += "/%DCN124%/";
} else if (key[i] == 126) {
key_return += "/%DCN126%/";
} else {
key_return += key[i];
}
}
return key_return;
}
/**
* Sends raw command to hub. Direct invocation by sub-classes not recommended
* if not extremely required.<br>
* <br>
* <b>Note:</b> Always make use of outThread for writing on socket as it may block.
*
* @param buffer
* Line which needs to be send. This method won't append "|" on the end on the string if it doesn't exist, so it is up to make
* sure buffer ends with "|" if you calling this method.
* @throws IOException On error while sending data into the socket.
*/
protected final void SendCommand(final String buffer) throws IOException {
if (output != null) {
SendCommand(buffer, output);
}
}
/**
* Reading command before InputThread is started (only for connecting).
* @throws IOException On error while sending data into the socket.
* @return Command from hub
*/
protected final String ReadCommand() throws IOException {
return ReadCommand(input);
}
/**
* Handled internally. <b>DO NOT CALL.</b>
*/
public final void onUDPExceptionClose(IOException e) {
_udp_inputThread = null;
try {
initiateUDPListening();
} catch (SocketException e1) {
logger.error("Failed to reopen UDP port. Searching may not work.", e1);
}
}
//********Methods to send commands to the hub***********/
/**
* Sends public message on main chat.
*
* @param message Message to be sent. It shouldn't end with "|".
*/
public final void SendPublicMessage(final String message) {
outThread.invokeLater(new Runnable() {
@Override
public void run() {
try {
SendCommand("<" + _botname + "> " + escapeSpecial(message, false) + "|");
} catch (IOException e) {
logger.error("Private message send failed.", e);
dispatchThread.callOnSendCommandFailed("Could not send public message. Got error: " + e.getMessage(),
e, JMethod.PUBLIC_MSG);
}
}
});
}
/**
* Sends private message to specified user.
*
* @param user User who will get message.
* @param message Message to be sent. It shouldn't end with "|".
*/
public final void SendPrivateMessage(final String user, final String message) {
outThread.invokeLater(new Runnable() {
@Override
public void run() {
try {
SendCommand("$To: " + user + " From: " + _botname + " $<" + _botname + "> "
+ escapeSpecial(message, false) + "|");
} catch (IOException e) {
logger.error("Private message send failed.", e);
dispatchThread.callOnSendCommandFailed("Could not send private message. Got error: " + e.getMessage(),
e, JMethod.PRIVATE_MSG);
}
}
});
}
/**
* Kicks specified user. Note that bot has to have permission to do this
*
* @param user User to be kicked
*/
public final void KickUser(User user) {
if (!isConnected())
return;
try {
SendCommand("$Kick " + user.username() + "|");
} catch (Exception e) {}
}
/**
* Kicks specified user. Note that bot has to have permission to do this
*
* @param user User to be kicked
*/
public final void KickUser(final String user) {
if (!isConnected())
return;
outThread.invokeLater(new Runnable() {
@Override
public void run() {
try {
SendCommand("$Kick " + user + "|");
} catch (IOException e) {}
}
});
}
/**
* This method serves to send message to all users on the hub. Note that most of the hubs have a flood detection system, so you will want to
* set timeout interval between two message sendings, or we will get warn and/or kicked!
*
* @param message Message to be send to all users
* @param timeout Timeout interval in milliseconds between sending to two consecutive user
*/
public final void SendAll(String message, long timeout) {
um.SendAll(message, timeout);
}
/**
* Searches in the hub.
* <p></p>
* <strong>Error not thrown.</strong> onSendCommandFailed(String,Throwable,JMethod.SEARCH)
* will be invoked in case of error.
* @param what The term to search for as per constrains given.
*/
public final void Search(final SearchSet what) {
if (!isConnected())
return;
outThread.invokeLater(new Runnable() {
@Override
public void run() {
long size =
what.size_criteria == SearchSet.SizeCriteria.NONE ? 0 : (what.size_unit == SearchSet.SizeUnit.BYTE ? what.size
: what.size_unit.getValue() * 1024 * what.size);
String cmd = "$Search ";
if (_passive) {
cmd = cmd + "Hub:" + _botname + " ";
} else {
cmd = cmd + _botIP + ":" + _udp_port + " ";
}
String search = (what.size_criteria == SearchSet.SizeCriteria.NONE ? "F" : "T") + "?";
search =
search
+ (what.size_criteria == SearchSet.SizeCriteria.NONE || what.size_criteria == SearchSet.SizeCriteria.ATLEAST ? "F"
: "T") + "?";
search = search + (what.size_criteria == SearchSet.SizeCriteria.NONE ? "0" : size) + "?";
search = search + what.data_type.getValue() + "?";
search = search + (what.data_type == SearchSet.DataType.TTH ? "TTH:" : "") + escapeSpecial(what.string, true);
cmd = cmd + search + "|";
logger.debug("from bot: " + cmd);
try {
SendCommand(cmd);
} catch (IOException e) {
logger.error("Search failed.", e);
dispatchThread.callOnSendCommandFailed("Search failed. Got error: " + e.getMessage(),
e, JMethod.SEARCH);
}
}
});
}
/**
* Method for returning search results to active clients. You should use it carefully if you're not owner/super user of the hub 'cause this can
* gets you banned/kicked. Search result you will return here are imaginary (same as your sharesize).
*
* @param IP
* IP address that gave us user who was searching for returning results
* @param port
* Port that gave us user who was searching for returning results
* @param isDir
* Set true if you're returning directory, false if it is a file
* @param name
* Name of the file/dir you're returning. Note that some clients reject names that are note like the one they were searching
* for. This means that if someone were searching for 'firefox', and we're returned 'opera', his client won't display our
* result.
* @param size
* Size of the file in bytes we're returning
* @param free_slots
* How many slots we have opened/unused
*/
private void SendActiveSearchReturn(String IP, int port, boolean isDir, String name, String hash, long size, int free_slots) {
name = sanitizePath(escapeSpecial(name, false));
StringBuffer buffer = new StringBuffer();
String hub_ip = _ip.toString();
if (hub_ip.contains("/")) {
hub_ip = hub_ip.substring(hub_ip.indexOf('/') + 1);
}
char c = 5;
if (isDir == true) {
buffer.append("$SR " + _botname + " " + name);
buffer.append(" " + free_slots + "/" + _maxUploadSlots);
buffer.append(c);
buffer.append(_hubname + " (" + hub_ip + ":" + _port + ")|");
} else {
buffer.append("$SR " + _botname + " " + name);
buffer.append(c);
buffer.append(size + " " + free_slots + "/" + _maxUploadSlots);
buffer.append(c);
buffer.append((hash == null ? _hubname : "TTH:" + hash) + " (" + hub_ip + ":" + _port + ")|");
}
logger.debug("from bot: " + buffer);
try {
DatagramSocket ds = new DatagramSocket();
byte[] bytes = new byte[buffer.length()];
for (int i = 0; i < buffer.length(); i++)
bytes[i] = (byte) buffer.charAt(i);
InetAddress address = InetAddress.getByName(IP);
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, address, port);
ds.send(packet);
} catch (Exception e) {
logger.error("Exception in SendActiveSearchReturn.", e);
}
}
/**
* Method for returning search results to passive clients. You hould use it carefully if you're not owner/super user of the hub 'cause this
* can gets you banned/kicked. Search result you will return here are imaginary (same as your sharesize).
*
* @param user
* User who was searching. Since he is in passive mode, we return result to hub
* @param isDir
* Set true if you're returning directory, false if it is a file
* @param name
* Name of the file/dir you're returning. Note that some clients reject names that are note like the one they were searching
* for. This means that if someone were searching for 'firefox', and we're returned 'opera', his client won't display our
* result.
* @param size
* Size of the file in bytes we're returning
* @param free_slots
* How many slots we have opened/unused
*/
private void SendPassiveSearchReturn(String user, boolean isDir, String name, String hash, long size, int free_slots) {
name = sanitizePath(escapeSpecial(name, false));
StringBuffer buffer = new StringBuffer();
String hub_ip = _ip.toString();
if (hub_ip.contains("/")) {
hub_ip = hub_ip.substring(hub_ip.indexOf('/') + 1);
}
char c = 5;
if (isDir == true) {
buffer.append("$SR " + _botname + " " + name);
buffer.append(" " + free_slots + "/" + _maxUploadSlots);
buffer.append(c);
buffer.append((hash == null ? _hubname : "TTH:" + hash) + " (" + hub_ip + ":" + _port + ")");
buffer.append(c);
buffer.append(user + "|");
} else {
buffer.append("$SR " + _botname + " " + name);
buffer.append(c);
buffer.append(size + " " + free_slots + "/" + _maxUploadSlots);
buffer.append(c);
buffer.append(_hubname + " (" + hub_ip + ":" + _port + ")");
buffer.append(c);
buffer.append(user + "|");
}
logger.debug("from bot: " + buffer);
try {
SendCommand(buffer.toString());
} catch (Exception e) {
logger.error("Exception in SendPassiveSearchReturn.", e);
}
}
//******Methods that are called to notify an event******/
/**
* Called when a <u>active</u> user is searching for something.<br>
* You need not override this method as it has the code that automatically searches
* in the file list and returns the result to the user.<br>
* If you need to implement a feature e.g. a search spy, etc. override
* {@link #onActiveSearch(String, int, SearchSet)}.
* @param ip The IP of the user who made the search.
* @param port The port to which the result data datagram should be sent.
* @param search The search that was made.
*/
final void onSearch(final String ip, final int port, final SearchSet search) {
if (shareManager == null)
return;
final List<User> u = um.getUserByIP(ip, false);
searchThread.invokeLater(new Runnable() {
@Override
public void run() {
/*
* Certainty probability (cp) calculation is done as follows:-
* 1) if no user with IP == ip found then cp = 0.0
* 2) else {
* a) if some users are found with IP = ip then cp = 1 / no. of users found.
* b) if bot is an operator and hub supports UserIP2 then the user list we
* created above is indeed correct and cp is not modified at all.
* else
* The list we prepared above may not be correct as we may not have
* all user's IPs, so we arbitrarily subtract 0.1 from cp.
* }
*/
List<SearchResultSet> res =
shareManager.searchOwnFileList(search, MAX_RESULTS_ACTIVE, u.size() == 0 ? null : u.get(0), u.size() == 0 ? 0.0 : 1
/ u.size() - (_op && isHubSupports("UserIP2") ? 0 : 0.1));
if (res != null) {
for (SearchResultSet r : res)
SendActiveSearchReturn(ip, port, r.isDir, r.name, r.TTH.isEmpty() || r.TTH == null ? null : r.TTH, r.size,
getFreeUploadSlots());
}
}
});
}
/**
* Called when a <u>passive</u> user is searching for something.<br>
* You need not override this method as it has the code that automatically searches
* in the file list and returns the result to the user.<br>
* If you need to implement a feature e.g. a search spy, etc. override
* {@link #onPassiveSearch(String, SearchSet)}.
* @param user The user who made the search.
* @param search The search that was made.
*/
final void onSearch(final String user, final SearchSet search) {
if (shareManager == null)
return;
searchThread.invokeLater(new Runnable() {
@Override
public void run() {
List<SearchResultSet> res = shareManager.searchOwnFileList(search, MAX_RESULTS_PASSIVE, getUser(user), 1.0);
if (res != null) {
for (SearchResultSet r : res)
SendPassiveSearchReturn(user, r.isDir, r.name, r.TTH.isEmpty() || r.TTH == null ? null : r.TTH, r.size,
getFreeUploadSlots());
}
}
});
}
/**
* Called when receiving a search result from any user or the hub (in case you are passive).
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @param senderNick The user's nick who returned the result.
* @param senderIP This can be null if search response is received
* from the hub, i.e. you are passive.
* @param senderPort This is zero when you are passive.
* @param result The search response.
* @param free_slots The number of free slots <i>senderNick</i> user has.
* @param total_slots The total number of upload slots <i>senderNick</i> user has.
* @param hubName This is empty when TTH in <i>result</i> is set.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onSearchResult(String senderNick, String senderIP, int senderPort, SearchResultSet result, int free_slots,
int total_slots, String hubName) {}
/**
* Called upon successfully connecting to hub.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onConnect() {}
/**
* Called just when a new connection has been established with another client in Active mode.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onConnect2Client() {}
/**
* It is called when the bot quits. Just after it quits, as a side-effect of closing the socket, the onDisconnect() too is called.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onBotQuit() {}
/**
* Called upon disconnecting from hub.
*
* @since 1.0 The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required,
* also this method is now called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onDisconnect() {}
/**
* Called when public message is received.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @param user User who sent message.
* @param message Contents of the message.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onPublicMessage(String user, String message) {}
/**
* Called when user enter the hub.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @param user User name of the user who entered hub.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onJoin(String user) {}
/**
* Called when user quits hub.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @param user User name of user who quited hub.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onQuit(String user) {}
/**
* Called when some new info about the user is found. Like his IP, Passive/Active state, etc.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.<br>
* <b>Note:</b> This method is called by <b>User</b> and <b>UserManager</b> using <i>jDCBot-EventDispatchThread</i> thread.
* @param user The user from the hub.
*/
protected void onUpdateMyInfo(String user) {}
/**
* Called when bot receives private message.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @param user Name of user who sent us private message.
* @param message Contents of private message.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onPrivateMessage(String user, String message) {}
/**
* Called when channel message in channel where bot is present is received.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
*
* @param user Name of the user who sent message.
* @param channel Channel on which message is sent.
* @param message Contents of the channel message.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onChannelMessage(String user, String channel, String message) {}
/**
* Called when user in passive mode is searching for something. For specific details, (like meaning of dataType field and syntax of
* searchPattern) you should consult direct connect protocol documentation like:
* http://dc.selwerd.nl/doc/Command_Types_(client_to_server).html
*
* @deprecated Use {@link #onPassiveSearch(String, SearchSet)} instead.
*
* @param user User who is searching
* @param isSizeRestricted
* true if user restricted search result for minimum/maximum file size. If false, isMinimumSize and size should not be used and
* has no meaning
* @param isMinimumSize
* true if user restricted his search to file that has minimum size, false if user restricted search result to maximum size.
* Used only if isSizeRestricted=true
* @param size
* Size that user restricted his search. Is it minimum od maximum size is contained in isMimimumSizeUsed only if
* isSizeRestricted=true
* @param dataType
* Type of the data user is searching for.
* @param searchPattern
* Pattern user is searching for.
*/
@Deprecated
protected void onPassiveSearch(String user, boolean isSizeRestricted, boolean isMinimumSize, long size, int dataType,
String searchPattern) {}
/**
* Called when user in passive mode is searching for something.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
* @param user The passive user who made the search.
* @param search Contains all the details abot the search made.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onPassiveSearch(String user, SearchSet search) {}
/**
* Called when user in passive mode is searching for something. For specific details, (like meaning of dataType field and syntax of
* searchPattern) you should consult direct connect protocol documentation like:
* http://dc.selwerd.nl/doc/Command_Types_(client_to_server).html
*
* @deprecated Use {@link #onActiveSearch(String, int, SearchSet)} instead.
*
* @param IP
* IP address user who was searching gave to deliver search results
* @param port
* Port user who was searching gave to deliver search results
* @param isSizeRestricted
* true if user restricted search result for minimum/maximum file size. If false, isMinimumSize and size should not be used and
* has no meaning
* @param isMinimumSize
* true if user restricted his search to file that has minimum size, false if user restricted search result to maximum size.
* Used only if isSizeRestricted=true
* @param size
* Size that user restricted his search. Is it minimum od maximum size is contained in isMimimumSizeUsed only if
* isSizeRestricted=true
* @param dataType
* Type of the data user is searching for.
* @param searchPattern
* Pattern user is searching for.
*/
@Deprecated
protected void onActiveSearch(String IP, int port, boolean isSizeRestricted, boolean isMinimumSize, long size, int dataType,
String searchPattern) {}
/**
* Called when user in active mode is searching for something.
* <p>
* The implementation of this method in the jDCBot abstract class performs no actions and may be overridden as required.
* @param ip The IP of the user who made the search.
* @param port The port to which the search result should be sent.
* @param search Contains all the details about the search made.
*
* @since 1.0 This method is called by using <i>jDCBot-EventDispatchThread</i> thread.
*/
protected void onActiveSearch(String ip, int port, SearchSet search) {}
/**
* Called when download is complete.<br>
* <b>Note:</b> This method is called by <b>DownloadHandler</b> using <i>jDCBot-EventDispatchThread</i> thread.
* @param user The user from whom the file was downloaded.
* @param due The informations about the file downloaded is in this.
* @param success It is true if download was successful else false.
* @param e The exception that occurred when success is false else it is null.
*/
protected void onDownloadComplete(User user, DUEntity due, boolean success, BotException e) {}
/**
* Called when download is starting.<br>
* <b>Note:</b> This method is called by <b>DownloadHandler</b> using <i>jDCBot-EventDispatchThread</i> thread.
* @param user
* @param due
*/
protected void onDownloadStart(User user, DUEntity due) {}
/**
* Called when upload is complete.<br>
* <b>Note:</b> This method is called by <b>DownloadHandler</b> using <i>jDCBot-EventDispatchThread</i> thread.
* @param user The user to whom the file was uploaded.
* @param due The informations about the file uploaded is in this.
* @param success It is true if upload was successful else false.
* @param e The exception that occurred when success is false else it is null.
*/
protected void onUploadComplete(User user, DUEntity due, boolean success, BotException e) {}
/**
* Called when upload is starting.<br>
* <b>Note:</b> This method is called by <b>DownloadHandler</b> using <i>jDCBot-EventDispatchThread</i> thread.
* @param user The user to whom the file is being uploaded.
* @param due The informations about the file downloaded is in this.
*/
protected void onUploadStart(User user, DUEntity due) {}
/**
* Called when async call to communicate with remote system fails.
* The methods which may result in the invocation of this will
* mention this in its comment.
* @param msg
* @param exception
* @param src The source method
*/
protected void onSendCommandFailed(String msg, Throwable exception, JMethod src) {}
/**
* Called when hub announces its name. Note
* that for different users the same hub may
* send different hub name, also the hub name
* may change several times during a single
* session, so do not use this to uniquely
* identify a hub. Instead use {@link User#getHubSignature()}.
* @param hubName Hub's name.
*/
protected void onHubName(String hubName) {}
}